#include "config.h"
#include <linux/delay.h>
#include <linux/bitops.h>
#include <linux/sched.h>
#include <linux/parport_pc.h>
#include "lcdmod-lib.h"

/* macros like in lp.c, using parport_pc, usefull, remember that they use struct lcddev! */
#define	r_dtr()		(parport_pc_read_data(lcddev.pd->port))
#define	r_ctr()		(parport_pc_read_control(lcddev.pd->port))
#define	w_dtr(y)	(parport_pc_write_data(lcddev.pd->port, (y)))
#define	w_ctr(y)	(parport_pc_write_control(lcddev.pd->port, (y)))

extern int base;
extern struct lcdmod_dev lcddev; /* lcdmod devices, see to lcdmod-lib.h */

DEFINE_MUTEX(hd44780_mutex); /* the inner mutex */

/* is_inst - returns 1 when RS_INST flag is set ( RS_INST or RS_DATA must be != 0 ); similar is_data */
#if RS_DATA != 0
#	define is_inst(x)	(~(x & RS_DATA))
#	define is_data(x)	  (x & RS_DATA)
#elif RS_INST != 0
#	define is_inst(x) 	  (x & RS_INST)
#	define is_data(x)	(~(x & RS_INST))
#else
#	error "Both RS_INST and RS_DATA are != 0"
#	error "One of them MUST be != 0"
#	error "Get back in the source and correct it"
#endif

static void _hd44780_sleep_on_write_(int flags, unsigned char command)
{
	if ( is_inst(flags) ) { 
		PDEBUG_hd44780(" ^ inst write sleep on cmd: %x\n", command);
		switch (command) {
		case 0x01:
		case 0x02:
		case 0x03:
			LONG_DELAY();
			return;
		default:
			NORMAL_DELAY();
			return;
		}
	}
	PDEBUG_hd44780(" ^ data write sleep on cmd: %x\n", command);
	NORMAL_DELAY(); 
}

static void _hd44780_sleep_on_read_(int flags) \
{
	if ( is_data(flags) ) {
		PDEBUG_hd44780(" ^ busyflag read sleep.\n");
	} else {
		PDEBUG_hd44780(" ^ data read sleep.\n");
		NORMAL_DELAY();
	}
}

static void _hd44780_counter_reset_(void)
{
	PDEBUG_hd44780(" ^ counter reset \n");
	base = 0;
	++lcddev.reset_num;
	w_ctr(RW_READ | RS_INST | CNT_NEXT);
	w_ctr(RW_READ | RS_INST | CNT_RESET);
	RESET_PULSE_WIDTH();
	w_ctr(RW_READ | RS_INST | CNT_OFF);
	RESET_SET_UP_TIME();
}

/**
 * hd44780_set_active_ctrl
 * sets active controler to ctrl_nr, and sets base = ctrl_nr 
 * it can go into endless loop */
static void _hd44780_set_active_ctrl_(unsigned int ctrl_nr)
{
	if (ctrl_nr < base) {
		_hd44780_counter_reset_();
	}
	while(base != ctrl_nr) {
		PDEBUG_hd44780(" ^ loop: %d ++ up to %d \n", base, ctrl_nr);
		++lcddev.next_num;
		w_ctr( RW_READ | RS_INST | CNT_NEXT);
		NEXT_PULSE_WIDTH();
		w_ctr( RW_READ | RS_INST | CNT_OFF);
		NEXT_SET_UP_TIME();
		if (base < CNT_LENGTH-1)
			++base;
		else
			base = 0;
	}
}

/* sets the flags/data for the controler */
static void _hd44780_capable_(int flags)
{
	w_ctr(flags | CNT_OFF);
	ADDRESS_SET_UP_TIME();
}
/* enables controler */
static void _hd44780_enable_(int flags)
{
	w_ctr(flags | CNT_ENABLE);
	ENABLE_PULSE_WIDTH();
}
/* disables controler */
static void _hd44780_disable_(int flags)
{
	w_ctr(flags | CNT_OFF);
	DATA_HOLD_TIME();
}

/** _hd44780_wait_for_busyflag_
 * wait for busyflag to go low on setted controler
 * mutex is locked, counter is set
 */
static void _hd44780_wait_for_ready_(void)
{
	/* here could be cursor getting/checking */
	unsigned int count;
	PDEBUG_hd44780(" ^ waiting for ctrl %d to lower the busyflag.\n", NUM_CONTROLLERS);
	
	_hd44780_capable_(RW_READ | RS_INST);
	parport_data_reverse(lcddev.pd->port);
	_hd44780_enable_(RW_READ | RS_INST);
	for(count = 1; (count < BUSYFLAG_STUCK) && (r_dtr() & 0x80); ++count) {
		_hd44780_disable_(RW_READ | RS_INST);
		
		BUSYFLAG_DELAY();
		
		_hd44780_enable_(RW_READ | RS_INST);
	}
	_hd44780_disable_(RW_READ | RS_INST);
	lcddev.bfchecks_num+=count;
	
	if (count == BUSYFLAG_STUCK) {
		PWARN("Reading busyflag ctrl %d stuck: checked %d times!", base, BUSYFLAG_STUCK);
		PWARN("Disabling busyflag checking on this port!\n");
		set_bit(FLAG_NOBFCHECK, &lcddev.flags);
	}
}

static unsigned char _hd44780_read(unsigned int ctrl_nr, int flags)
{
	unsigned char status;
	
	/** prepare counter, port and ctrl */
	_hd44780_set_active_ctrl_(ctrl_nr);
	if (!test_bit(FLAG_NOBFCHECK, &lcddev.flags))
		_hd44780_wait_for_ready_();
	/** read data */
	parport_data_reverse(lcddev.pd->port);
	_hd44780_capable_(flags);
	_hd44780_enable_(flags);
	READ_EXTRA_DELAY();
	status = r_dtr();
	_hd44780_disable_(flags);
	
	if (test_bit(FLAG_NOBFCHECK, &lcddev.flags))
		_hd44780_sleep_on_read_(flags);
		
	return status;
}

static void _hd44780_write(unsigned int ctrl_nr, int flags, unsigned char command)
{
	/** prepare counter, port and ctrl */
	_hd44780_set_active_ctrl_(ctrl_nr);
	if (!test_bit(FLAG_NOBFCHECK, &(lcddev.flags)))
		_hd44780_wait_for_ready_();
	/** write data */
	parport_data_forward(lcddev.pd->port);
	w_dtr(command);
	_hd44780_capable_(flags);
	_hd44780_enable_(flags);
	_hd44780_disable_(flags);
	w_dtr(0x00);
	parport_data_reverse(lcddev.pd->port);
	
	if (test_bit(FLAG_NOBFCHECK, &(lcddev.flags)))
		_hd44780_sleep_on_write_(flags, command);
}



/** export functions */
unsigned char hd44780_read(unsigned int ctrl_nr, int flags)
{
	unsigned char status;
	PDEBUG_hd44780("read: %d %d %s %#2x act %d lcddev->flags:%lx \n", 
		ctrl_nr, flags ? "1" : "2", base, lcddev.flags);
	
	/* protect against endless loop in _set_active_ctrl_, check input values */
	/*if (ctrl_nr >= NUM_CONTROLLERS) return 0xf0;
	if ( >= ports_number) return 0xf0;
	if (ctrl_nr >= CNT_LENGTH) return 0xf0;*/
	
	flags |= RW_READ;
	
	mutex_lock(&hd44780_mutex);
	
	++lcddev.read_num;
	status = _hd44780_read(ctrl_nr, flags);
	
	mutex_unlock(&hd44780_mutex); 
	
	PDEBUG_hd44780("   ^ readed %2x %c\n", status, status);
	return status;
}

void hd44780_write(unsigned int ctrl_nr, int flags, unsigned char command)
{
	PDEBUG_hd44780("write: %d %d %s %#2x act %d lcddev->flags:%lx\n", 
		ctrl_nr, flags ? "1" : "2", command, base, lcddev.flags);
	/* protect against endless loop in _set_active_ctrl_, check input values */
	/* if ( >= ports_number) return;
	if (ctrl_nr >= NUM_CONTROLLERS) return;
	if (ctrl_nr >= CNT_LENGTH) return;*/
	
	flags |= RW_WRITE;
	
	mutex_lock(&hd44780_mutex);
	
	++lcddev.write_num;
	_hd44780_write(ctrl_nr, flags, command);
	
	mutex_unlock(&hd44780_mutex); 
}

void hd44780_write_all(int flags, unsigned char command)
{
	unsigned int j;
	for(j = 0; j < NUM_CONTROLLERS; j++) {
		hd44780_write(j, flags, command);
	}
}

/** init the port counter and maybe other stuff */
void port_init(void)
{
	mutex_lock(&hd44780_mutex);
	w_dtr(0x00);
	parport_data_reverse(lcddev.pd->port);
	/* reset the counter 
	 * from now on, base is the value of the active controler 
	 */
	_hd44780_counter_reset_();
	mutex_unlock(&hd44780_mutex);
}
